值类型与引用类型
由于Solidity是一个静态类型的语言,所以编译时需明确指定变量的类型(包括本地变量
或状态变量
),Solidity
编程语言提供了一些基本类型(elementary types)
可以用来组合成复杂类型。
类型可以与不同运算符组合,支持表达式运算,可以通过表达式的执行顺序来了解执行顺序。
值类型(Value Type)
值类型
包含
布尔(Booleans)
整型(Integer)
地址(Address)
定长字节数组(fixed byte arrays)
有理数和整型(Rational and Integer Literals
,String literals)
枚举类型(Enums)
函数(Function Types)
为什么会叫值类型
,是因为上述这些类型在传值时,总是值传递1。比如在函数传参数时,或进行变量赋值时。
引用类型(Reference Types)
复杂类型,占用空间较大的。在拷贝时占用空间较大。所以考虑通过引用传递。常见的引用类型有:
- 不定长字节数组(bytes)
- 字符串(string)
- 数组(Array)
- 结构体(Struts)
布尔(Booleans)
bool
: 可能的取值为常量值true
和false
.
支持的运算符:
- !逻辑非
- && 逻辑与
- || 逻辑或
- == 等于
- != 不等于
备注:运算符&&
和||
是短路运算符,如f(x)||g(y)
,当f(x)
为真时,则不会继续执行g(y)
。
整型(Integer)
int/uint
:变长的有符号或无符号整型。变量支持的步长以8
递增,支持从uint8
到uint256
,以及int8
到int256
。需要注意的是,uint
和int
默认代表的是uint256
和int256
。256是标识最大数字为2的256次方。
支持的运算符:
- 比较:
<=
,<
,==
,!=
,>=
,>
,返回值为bool
类型。 - 位运算符:
&
,|
,(^
异或),(~
非)。 - 数学运算:
+
,-
,一元运算+
,*
,/
,(%
求余),(**
平方)。
整数除法总是截断的,但如果运算符是字面量,则不会截断(后面会进一步提到)。另外除0
会抛异常 ,我们来看看下面的这个例子:
|
|
整数字面量
整数字面量,由包含0-9的数字序列组成,默认被解释成十进制。在Solidity
中不支持八进制,前导0
会被默认忽略,如0100
,会被认为是100
。
小数由.
组成,在他的左边或右边至少要包含一个数字。如1.
,.1
,1.3
均是有效的小数。
字面量本身支持任意精度,也就是可以不会运算溢出,或除法截断。但当它被转换成对应的非字面量类型,如整数或小数。或者将他们与非字面量进行运算,则不能保证精度了。
|
|
总之来说就是,字面量怎么都计算都行,但一旦转为对应的变量后,再计算就不保证精度啦。
地址(Address)
地址
地址: 以太坊地址的长度,大小20个字节,160位,所以可以用一个uint160
编码。地址是所有合约的基础,所有的合约都会继承地址对象,也可以随时将一个地址串,得到对应的代码进行调用。当然地址代表一个普通帐户时,就没有这么多丰富的功能啦。
支持的运算符
<=
,<
,==
,!=
,>=
和>
地址类型的成员
属性:balance
函数:send()
,call()
,delegatecall()
,callcode()
。
地址字面量
十六进制的字符串,凡是能通过地址合法性检查(address checksum test)2,就会被认为是地址,如0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
。需要注意的是39到41位长的没有通过地址合法性检查的,会提示一个警告,但会被视为普通的有理数字面量。
balance
通过它能得到一个地址的余额。
|
|
this
如果只是想得到当前合约的余额,其实可以这样写:
|
|
原因是对于合约来说,地址代表的就是合约本身,合约对象默认继承自地址对象,所以内部有地址的属性。
地址的方法send()
用来向某个地址发送货币(货币单位是wei
)。
|
|
这个合约实现的是充值。this.send(msg.value)
意指向合约自身发送msg.value
量的以太币。msg.value
是合约调用方附带的以太币。
send()
方法执行时有一些风险
- 调用递归深度不能超1024。
- 如果
gas
不够,执行会失败。 - 所以使用这个方法要检查成功与否。或为保险起见,货币操作时要使用一些最佳实践。
如果执行失败,将会回撤所有交易,所以务必留意返回结果。
call()
,callcode()
和delegatecall()
为了同一些不支持ABI协议的进行直接交互(一般的web3.js
,soldity
都是支持的)。可以使用call()
函数,用来向另一个合约发送原始数据。参数支持任何类型任意数量。每个参数会按规则(规则是按ABI4)打包成32字节并一一拼接到一起。
call()
方法支持ABI协议[ABI]定义的函数选择器。如果第一个参数恰好4个字节,在这种情况下,会被认为根据ABI协议定义的函数器指定的函数签名[ABI]。所以如果你只是想发送消息体,需要避免第一个参数是4个字节。
call
方法返回一个bool
值,以表明执行成功还是失败。正常结束返回true
,异常终止返回false
。我们无法解析返回结果,因为这样我们得事前知道返回的数据的编码和数据大小(这里的潜在假设是不知道对方使用的协议格式,所以也不会知道返回的结果如何解析,有点祼协议测试的感觉)。
同样我们也可以使用delegatecall()
,它与call
方法的区别在于,仅仅是代码会执行,而其它方面,如(存储,余额等)都是用的当前的合约的数据。delegatecall()
方法的目的是用来执行另一个合约中的工具库。所以开发者需要保证两个合约中的存储变量能兼容,来保证delegatecall()
能顺利执行。
在homestead阶段之前,仅有一个受限的多样的callcode()
方法可用,但并未提供对msg.sender
,msg.value
的访问权限。
上面的这三个方法call()
,delegatecall()
,callcode()
都是底层的消息传递调用,最好仅在万不得已才进行使用,因为他们破坏了Solidity的类型安全。
关于call()
函数究竟发的什么消息体,函数选择器究竟怎么用,参见这个文章的挖掘。
上述的函数都是底层的函数,使用时要异常小心。当调用一个未知的,可能是恶意的合约时,当你把控制权交给它,它可能回调回你的合约,所以要准备好在调用返回时,应对你的状态变量可能被恶意篡改的情况。
- 如果你想了解更多关于地址的由来,UTXO等,可以参考: http://me.tryblockchain.org/Solidity%E7%9A%84%E5%9C%B0%E5%9D%80%E7%B1%BB%E5%9E%8B.html ↩
- 为防止录入地址有误,一种格式化地址后来确认地址有效性的方案,https://github.com/ethereum/EIPs/issues/55 ↩
- 原因详见实现以太币支付的文章,http://me.tryblockchain.org/%E6%94%AF%E4%BB%98%E7%9B%B8%E5%85%B3.html ↩
- 关于ABI协议的详细说明:http://me.tryblockchain.org/Solidity-abi-abstraction.html ↩
字节数组(byte arrays)
定长字节数组(Fixed-size byte arrays)
bytes1
, … ,bytes32
,允许值以步长1
递增。byte
默认表示byte1
。
运算符
比较:<=
,<
,==
,!=
,>=
,>
,返回值为bool
类型。
位运算符:&
,|
,^
(异或),~
非
支持序号的访问,与大多数语言一样,取值范围[0, n),其中n
表示长度。
成员变量
.length
表示这个字节数组的长度(只读)。
动态大小的字节数组
bytes
: 动态长度的字节数组,参见数组(Arrays)。非值类型1。
string
: 动态长度的UTF-8编码的字符类型,参见数组(Arrays)。非值类型[valueType]。
一个好的使用原则是:
bytes
用来存储任意长度的字节数据,string
用来存储任意长度的UTF-8
编码的字符串数据。- 如果长度可以确定,尽量使用定长的如
byte1
到byte32
中的一个,因为这样更省空间。
枚举
枚举类型是在Solidity中的一种用户自定义类型。他可以显示的转换与整数进行转换,但不能进行隐式转换。显示的转换会在运行时检查数值范围,如果不匹配,将会引起异常。枚举类型应至少有一名成员。我们来看看下面的例子吧。
|
|
函数(Function Types)
函数类型1即是函数这种特殊的类型。
- 可以将一个函数赋值给一个变量,一个函数类型的变量。
- 还可以将一个函数作为参数进行传递。
- 也可以在函数调用中返回一个函数。
函数类型有两类;可分为internal
和external
函数。
内部函数(internal)
因为不能在当前合约的上下文环境以外的地方执行,内部函数只能在当前合约内被使用。如在当前的代码块内,包括内部库函数,和继承的函数中。
外部函数(External)
外部函数由地址和函数方法签名两部分组成。可作为外部函数调用
的参数,或者由外部函数调用
返回。
函数的定义
完整的函数的定义如下:
|
|
若不写类型,默认的函数类型是internal
的。如果函数没有返回结果,则必须省略returns
关键字。下面我们通过一个例子来了解一下。
|
|
如果一个函数变量没有初始化,直接调用它将会产生异常。如果delete
了一个函数后调用,也会发生同样的异常。
如果外部函数
类型在Solidity
的上下文环境以外的地方使用,他们会被视为function
类型。编码为20字节的函数所在地址,紧跟4字节的函数方法签名2的共占24字节的bytes24
类型。
合约中的public
的函数,可以使用internal
和external
两种方式来调用。下面来看看,两种方式的不同之处。
函数的internal
与external
:
调用一个函数f()
时,我们可以直接调用f()
,或者使用this.f()
。但两者有一个区别。前者是通过internal
的方式在调用,而后者是通过external
的方式在调用。请注意,这里关于this
的使用与大多数语言相背。下面通过一个例子来了解他们的不同:
|
|
数组
数组
可以声明时指定长度,或者是变长的。对storage
1的数组来说,元素类型可以是任意的,类型可以是数组,映射类型,数据结构等。但对于memory
[datalocation]的数组来说。如果函数是对外可见的2,那么函数参数不能是映射类型的数组,只能是支持ABI的类型3。
一个类型为T,长度为k的数组,可以声明为T[k]
,而一个变长的数组则声明为T[]
。
你还可以声明一个多维数据,如一个类型为uint
的数组长度为5的变长数组,可以声明为uint[][5] x
。需要留心的是,相比非区块链语言,多维数组的长度声明是反的。
要访问第三个动态数据的,第二个元素,使用x[2][1]
。数组的序号是从0开始的,序号顺序与定义相反。
bytes
和string
是一种特殊的数组。bytes
类似byte[]
,但在外部函数作为参数调用中,会进行压缩打包,更省空间,所以应该尽量使用bytes
4。string
类似bytes
,但不提供长度和按序号的访问方式。
由于bytes
与string
,可以自由转换,你可以将字符串s
通过bytes(s)
转为一个bytes
。但需要注意的是通过这种方式访问到的是UTF-8编码的码流,并不是独立的一个个字符。比如中文编码是多字节,变长的,所以你访问到的很有可能只是其中的一个代码点。
类型为数组的状态变量,可以标记为public
类型,从而让Solidity
创建一个访问器,如果要访问数组的某个元素,指定数字下标就好了。
创建一个数组
可使用new
关键字创建一个memory
的数组。与stroage
数组不同的是,你不能通过.length
的长度来修改数组大小属性。我们来看看下面的例子:
|
|
在上面的代码中,f()
方法尝试调整数组a
的长度,编译器报错Error: Expression has to be an lvalue.
。但在g()
方法中我们看到可以修改5。
字面量及内联数组
数组字面量,是指以表达式方式隐式声明一个数组,并作为一个数组变量使用的方式。下面是一个简单的例子:
|
|
通过数组字面量,创建的数组是memory
的,同时还是定长的。元素类型则是使用刚好能存储的元素的能用类型,比如代码里的[1, 2, 3]
,只需要uint8
即可存储。由于g()
方法的参数需要的是uint
(默认的uint
表示的其实是uint256
),所以要使用uint(1)
来进行类型转换。
还需注意的一点是,定长数组,不能与变长数组相互赋值,我们来看下面的代码:
|
|
限制的主要原因是,ABI不能很好的支持数组,已经计划在未来移除这样的限制。(当前的ABI接口,不是已经能支持数组了?)
数组的属性和方法
length属性
数组有一个.length
属性,表示当前的数组长度。storage
的变长数组,可以通过给.length
赋值调整数组长度。memory
的变长数组不支持。
不能通过访问超出当前数组的长度的方式,来自动实现上面说的这种情况。memory
数组虽然可以通过参数,灵活指定大小,但一旦创建,大小不可调整,对于变长数组,可以通过参数在编译期指定数组大小。
push方法
storage
的变长数组和bytes
都有一个push()
,用于附加新元素到数据末端,返回值为新的长度。
|
|
结构体(struct)
Solidity
提供struct
来定义自定义类型。我们来看看下面的例子:
|
|
上面的代码向我们展示的一个简化版的众筹项目,其实包含了一些struct
的使用。struct
可以用于映射和数组中作为元素。其本身也可以包含映射和数组等类型。
我们不能声明一个struct
同时将这个struct
作为这个struct的一个成员。这个限制是基于结构体的大小必须是有限的。
虽然数据结构能作为一个mapping
的值,但数据类型不能包含它自身类型的成员,因为数据结构的大小必须是有限的。
需要注意的是在函数中,将一个struct
赋值给一个局部变量(默认是storage类型),实际是拷贝的引用,所以修改局部变量值时,会影响到原变量。
当然,你也可以直接通过访问成员修改值,而不用一定赋值给一个局部变量,如campaigns[comapingnId].amount = 0
映射/字典(mappings)
映射
或字典类型,一种键值对的映射关系存储结构。定义方式为mapping(_KeyType => _KeyValue)
。键的类型允许除映射
外的所有类型,如数组,合约,枚举,结构体。值的类型无限制。
映射
可以被视作为一个哈希表,其中所有可能的键已被虚拟化的创建,被映射到一个默认值(二进制表示的零)。但在映射表中,我们并不存储键的数据,仅仅存储它的keccak256
哈希值,用来查找值时使用。
因此,映射
并没有长度,键集合(或列表),值集合(或列表)这样的概念。
映射
类型,仅能用来定义状态变量
,或者是在内部函数中作为storage
类型的引用。引用是指你可以声明一个,如var storage mappVal
的用于存储状态变量的引用的对象,但你没办法使用非状态变量来初始化这个引用。
可以通过将映射
标记为public
,来让Solidity创建一个访问器。要想访问这样的映射
,需要提供一个键值做为参数。如果映射
的值类型也是映射
,使用访问器访问时,要提供这个映射
值所对应的键,不断重复这个过程。下面来看一个例子:
|
|
由于调试时,你不一定方便知道自己的发起地址,所以把这个函数,略微调整了一下,以在调用时,返回调用者的地址。编译上述合同后,可以先调用update()
,执行成功后,查看调用信息,能看到你更新的地址,这样再查一下这个地址的在映射里存的值。